import math
from itertools import permutations,combinations
from shapely.geometry import  Polygon as ShapelyPolygon
from shapely import geometry
from shapely.ops import cascaded_union
from shapely.validation import explain_validity
import time
import networkx as nx
import graph
import hull
from collections import namedtuple

precision = 100

def nearly_equal(a,b):
    return abs(a-b) < precision

class Point():
    id = 0
    def __init__(self,origin):
        self.origin = origin
        self.splines = []
        self.segments = []
        self.x = origin.co.x
        self.y = origin.co.y
        self.z = origin.co.z
        self.id = Point.id + 1
        Point.id = self.id

    def attach_segment(self,segment):
        self.segments.append(segment)
    
    def attach_spline(self,spline):
        self.splines.append(spline)

    def equals(self,p):
        return nearly_equal(self.x, p.x) and nearly_equal(self.y, p.y) and nearly_equal(self.z, p.z)

    def __eq__(self, other):
        return self.equals(other)

    def __hash__(self):
        return self.id

    def __repr__(self):
        return "Point: " + str(self.x) + " " + str(self.y) + " " + str(self.z)

    @property
    def links(self):
        points = [seg.end for seg in self.segments]
        return set(points)
    
    @staticmethod
    def findOrCreate(points, origin):
        tmp = Point(origin)
        for p in points:
            if p.equals(tmp):
                return p 
                
        points.append(tmp)
        return tmp

    @staticmethod
    def findOrCreatePoints(points,origins):
        ret = []
        for o in origins:
            p = Point.findOrCreate(points,o)
            ret.append(p)
        return ret

    def get_shapely_point(self):
        pass

    def draw(self, bpy, offset_x = 0, offset_y = 0, offset_z = 0):
        bpyscene = bpy.context.scene
        # Create an empty mesh and the object.
        mesh = bpy.data.meshes.new('Point')
        sphere = bpy.data.objects.new("Point", mesh)
        sphere.location.x = self.x + offset_x
        sphere.location.y = self.y + offset_y
        sphere.location.z = self.z + offset_z
        # Add the object into the scene.
        bpyscene.objects.link(sphere)

class Segment():
    def __init__(self, origin, origin_type, start, end):
        self.origin = origin
        self.origin_type = origin_type
        self.start = start
        self.end = end
        start.attach_segment(self)
        end.attach_segment(self)

    def equals(self,seg):
        if self.start.equals(seg.start) and self.end.equals(seg.end):
            return True

    def drawSegments(self,bpy,elements):
        count = 0
        verts = []  # the vertex array
        edges = []
        faces = []  # the face array
        for e in elements:
            verts.append((e.start.co.x,e.start.co.y,e.start.co.z))
            verts.append((e.end.co.x,e.end.co.y,e.end.co.z))
            edges.append((count,count+1))
            count += 2
        #create mesh and object
        mesh = bpy.data.meshes.new("Tetrahedron")
        object = bpy.data.objects.new("Tetrahedron",mesh)
        #set mesh location
        object.location = bpy.context.scene.cursor_location
        bpy.context.scene.objects.link(object)
        #create mesh from python data
        mesh.from_pydata(verts,edges,faces)
        mesh.update(calc_edges=False)

class Polygon():
    def __init__(self,_type):
        self.segments = []
        self.type = _type

    def get_first_segment(self):
        return self.segments[0]
    
    def get_last_segment(self):
        return self.segments[-1]
    
    def add_segment(self,segment):
        self.segments.append(segment)
    
    def is_closed_polygon(self):
        start_seg = self.get_first_segment()
        end_seg = self.get_last_segment()
        if len(self.segments) > 2 and start_seg.start.equals(end_seg.end):
            return True
        else:
            return False

    def clone(self):
        new_polygon = Polygon(self.type)
        for segment in self.segments:
            new_polygon.add_segment(segment)
        return new_polygon

    def branch(self,segments):
        polygons = []
        for seg in segments:
            polygon = self.clone()
            polygon.add_segment(seg)
            if polygon.is_closed_polygon():
                polygons.append(polygon)
            elif not polygon.is_self_intersect():
                polygons.append(polygon)
        return polygons

    def is_self_intersect(self):
        frist = self.get_first_segment()
        coords = [(frist.start.co.x,frist.start.co.y)]
        for seg in self.segments:
            coords.append((seg.end.co.x,seg.end.co.y))
        p = ShapelyPolygon(coords)
        r = explain_validity(p)
        if "Self-intersection" in r:
            return True
        else:
            print(r)
            return False

    def contains(self,other):
        pass

    def connected(self,other):
        return self.shapely.intersects(other.shapely) or self.share_point(other)
    


    def share_point(self,other):
        self_points = self.points
        other_points = other.points
        for self_point in self_points:
            for other_point in other_points:
                if self_point.equals(other_point):
                    return True

    def same_points(self,other):
        points1 = self.points
        points2 = other.points

        if len(points1) != len(points2):
            return False
        
        def find_same_in_points(points,p):
            for point in points:
                if point.equals(p):
                    return point

        for p1 in points1:
            if not find_same_in_points(points2,p1):
                return False
        for p2 in points2:
            if not find_same_in_points(points1,p2):
                return False
        return True

    @property
    def points(self):
        return [seg.start for seg in self.segments]

    @property
    def shapely(self):
        poly_points = [(p.x, p.y, 0) for p in self.points]
        if len(poly_points) >= 3:
            return geometry.Polygon(poly_points)
        elif len(poly_points) == 2:
            return geometry.LineString(poly_points)
        else:
            return geometry.Point(poly_points[0])

    @property
    def graph(self):
        G = nx.DiGraph()
        for seg in self.segments:
                G.add_edge(seg.start,seg.end)
        return G

    def rebuild_points(self,points):
        polygon = Polygon('rebuild')
        for seg in self.segments:
            start = Point.findOrCreate(points,seg.start.origin)
            end = Point.findOrCreate(points,seg.end.origin)
            new_seg = Segment(None,'rebuild',start,end)
            polygon.add_segment(new_seg)
        return polygon
        

    def draw(self, bpy, offset_x = 0, offset_y = 0, offset_z = 0):


        if len(self.segments) == 0:
            return

        verts = []  # the vertex array
        edges = []
        faces = []  # the face array
        face = []
        count = 0
        for e in self.segments:
            verts.append((e.start.x + offset_x,e.start.y + offset_y,e.start.z + offset_z))
            verts.append((e.end.x + offset_x,e.end.y + offset_y,e.end.z + offset_z))
            edges.append((count,count+1))
            face.append(count)
            face.append(count+1)
            count += 2
        faces.append(tuple(face))
        #create mesh and object
        mesh = bpy.data.meshes.new("Tetrahedron")
        object = bpy.data.objects.new("Tetrahedron",mesh)
        
        #set mesh location
        object.location = bpy.context.scene.cursor_location
        bpy.context.scene.objects.link(object)
        #create mesh from python data
        mesh.from_pydata(verts,edges,faces)
        mesh.update(calc_edges=False)

    @staticmethod
    def subgraphsConnect(polygons):
        #根据点连通分组
        G = nx.Graph()
        for polygon in polygons:
            for seg in polygon.segments:
                G.add_edge(seg.start,seg.end)
        #print([len(c) for c in sorted(nx.connected_components(G), key=len, reverse=True)])
        point_groups = [c for c in sorted(nx.connected_components(G), key=len, reverse=True)]
        new_polygons = []
        for points in point_groups:
            new_polygons.append(Polygon.createPolygonFromPoints(points))
        
        return new_polygons

    @staticmethod
    def convexUnionPolygons(polygons):
        #碰撞两个一起求凸包
        convex_polygons = []
        while polygons:
            p = polygons.pop()
            grouping = [p]
            for i in polygons:
                for gi in grouping:
                    if gi.shapely.intersects(i.shapely):
                        grouping.append(i)
                        polygons.remove(i)
                        break
            points = set([])
            for i in grouping:
                points.update(set([p.start for p in i.segments]))
            
            def get_new_point(pset):
                for p in points:
                    if p.x == pset[0] and p.y == pset[1]:
                        return Point(p.origin)

            convex = hull.convex([(p.x,p.y) for p in points])
            frist = nxt = convex.pop(0)
            convex_polygon = Polygon('convex_polygon')
            while convex:
                p1 = nxt
                p2 = convex.pop(0)
                point1 = get_new_point(p1)
                point2 = get_new_point(p2)
                seg = Segment(None,"convex_segment",point1,point2)
                convex_polygon.add_segment(seg)
                nxt = p2
            point1 = get_new_point(nxt)
            point2 = get_new_point(frist)
            seg = Segment(None,"convex_segment",point1,point2)
            convex_polygon.add_segment(seg)

            convex_polygons.append(convex_polygon)

            
        return convex_polygons

    @staticmethod    
    def createPolygonFromPoints(points):
        print('start compute')
        G = nx.DiGraph()
        for p in points:
            for seg in p.segments:
                G.add_edge(seg.start,seg.end)
        ret = list(nx.simple_cycles(G))
        points = [c for c in sorted(ret, key=len, reverse=True)][0]
        polygon = Polygon('wall_group')
        frist = nxt = points.pop(0)
        while points:
            p1 = nxt
            p2 = points.pop(0)
            point1 = Point(p1.origin)
            point2 = Point(p2.origin)
            seg = Segment(None,"wall_group_segment",point1,point2)
            polygon.add_segment(seg)
            nxt = p2
        point1 = Point(nxt.origin)
        point2 = Point(frist.origin)
        seg = Segment(None,"wall_group_segment",point1,point2)
        polygon.add_segment(seg)
        return polygon

    @staticmethod
    def createConvexPolygonFromPoints(points):
        def get_new_point(pset):
            for p in points:
                if p.x == pset[0] and p.y == pset[1]:
                    return Point(p.origin)
        convex = hull.convex([(p.x,p.y) for p in points])
        frist = nxt = convex.pop(0)
        polygon = Polygon('convex_polygon')
        while convex:
            p1 = nxt
            p2 = convex.pop(0)
            point1 = get_new_point(p1)
            point2 = get_new_point(p2)
            seg = Segment(None,"convex_segment",point1,point2)
            polygon.add_segment(seg)
            nxt = p2
        point1 = get_new_point(nxt)
        point2 = get_new_point(frist)
        seg = Segment(None,"convex_segment",point1,point2)
        polygon.add_segment(seg)
        return polygon


         

class Parser():
    def __init__(self,bpy):
        self.bpy = bpy

    def parseWall(self):
        points = []
        segments = []
        polygons = []
        bpy = self.bpy
        #添加墙
        wg = [x for x in bpy.data.groups if "建筑墙体" in x.name][0]
        wc = [x for x in wg.objects if "A2-建筑墙体_curve_.001" == x.name][0]
        for spline in wc.data.splines:
            polygon = Polygon('wall')
            for pp in combinations(Point.findOrCreatePoints(points,spline.points), 2):
                seg = Segment(spline,'wall_spline',pp[0],pp[1])
                segments.append(seg)
                polygon.add_segment(seg)
            polygons.append(polygon)
        return points, segments,Polygon.subgraphsConnect(polygons)


    def parseDoor(self):
        points = []
        segments = []
        polygons = []
        bpy = self.bpy
        #添加门
        dg = [x for x in bpy.data.groups if "平面门" in x.name][0]
        dc = [x for x in dg.objects if "P7-平面门_curve_.001" == x.name][0]
        #open door
        door_points = []

        G = nx.Graph()
        for spline in dc.data.splines:
            print('len door_points',len(door_points))
            spline_points = []
            for i in spline.points:
                spline_points.append(Point.findOrCreate(door_points,i))
            for j in spline.bezier_points:
                spline_points.append(Point.findOrCreate(door_points,j))
            for pp in combinations(spline_points,2):
                G.add_edge(pp[0],pp[1])
        
        print([len(c) for c in sorted(nx.connected_components(G), key=len, reverse=True)])

        point_groups = [c for c in sorted(nx.connected_components(G), key=len, reverse=True)]
        for ps in point_groups:
            try:
                print(len(ps))
                polygons.append(Polygon.createConvexPolygonFromPoints(ps))
            except Exception as e:
                print(e)
                

        return point_groups, segments, Polygon.convexUnionPolygons(polygons)

    
    def parseWindow(self):
        points = []
        segments = []
        polygons = []
        bpy = self.bpy
        #添加窗
        cg = [x for x in bpy.data.groups if "平面窗" in x.name][0]
        cc = [x for x in cg.objects if "P4-平面窗_curve_.001" == x.name][0]
        
        for spline in cc.data.splines:
            polygon = Polygon('window')
            for pp in combinations(Point.findOrCreatePoints(points,spline.points), 2):
                seg = Segment(spline,'window_spline',pp[0],pp[1])
                segments.append(seg)
                polygon.add_segment(seg)
            polygons.append(polygon)
        return points, segments, Polygon.convexUnionPolygons(polygons)

    def drawElements(self,elements):
        self.bpy.elements = elements
        count = 0
        for element in elements:
         
            element.draw(self.bpy,-0,0,1000)
            count += 1

    def parse_rooms(self,threadName, delay):
        wall_points, wall_segments, wall_polygons = self.parseWall()
        door_points, door_segments, door_polygons = self.parseDoor()
        window_points, window_segments, window_polygons = self.parseWindow()
        polygons = wall_polygons + door_polygons + window_polygons
        points = []
        objects = [p.rebuild_points(points) for p in polygons]
        #self.drawElements(polygons)
        print('compute..')
        ret = union_ploygon_with_hole_result(objects)
        self.drawElements(ret)
        print('done')

        # points = []
        # polygon1 = create_polygon_from_mesh(self.bpy,points,'xyy')
        # polygon2 = create_polygon_from_mesh(self.bpy,points,'xyy2')
        # # polygon1.draw(self.bpy,10000,0,0)
        # # polygon2.draw(self.bpy,10000,0,0)
        # self.drawElements(union_ploygon_with_hole_result([polygon1,polygon2]))
        
        #self.drawElements([polygon1,polygon2])


def union_ploygon_with_hole_result(polygons):
    orign_polygons = [p for p in polygons]
    def is_origin_polygon(polygon):
        for orign_poly in orign_polygons:
            if orign_poly.same_points(polygon):
                return True

    def is_cover_origin(polygon):
        for orign_poly in orign_polygons:
            if polygon.shapely.intersects(orign_poly.shapely):
                try:
                    ins = polygon.shapely.intersection(orign_poly.shapely)
                    if ins and ins.area > 0:
                        return True
                except Exception as e:
                    return True
            

    G = polygons.pop().graph
    while polygons:
        G = nx.compose(G,polygons.pop().graph)
    ret = list(nx.simple_cycles(G))
    print([len(c) for c in sorted(ret, key=len, reverse=True)])
    ret_polygons = []
    for points in ret:
        polygon = Polygon('room_group')
        frist = nxt = points.pop(0)
        while points:
            p1 = nxt
            p2 = points.pop(0)
            point1 = Point(p1.origin)
            point2 = Point(p2.origin)
            seg = Segment(None,"wall_group_segment",point1,point2)
            polygon.add_segment(seg)
            nxt = p2
        point1 = Point(nxt.origin)
        point2 = Point(frist.origin)
        seg = Segment(None,"wall_group_segment",point1,point2)
        polygon.add_segment(seg)
        is_origin = is_origin_polygon(polygon)
        print('is_origin: ',is_origin)
        if is_origin:
            continue
        area = polygon.shapely.area
        print('area: ' , area)        
        if area == 0:
            continue
        is_cover = is_cover_origin(polygon)
        print('is_cover: ', is_cover)        
        if is_cover:
            continue
        ret_polygons.append(polygon)
    print('union_ploygon_with_hole_result print(polygon)' ,len(ret_polygons))
    return ret_polygons
    #print([len(c) for c in sorted(ret, key=len, reverse=True)])




def create_polygon_from_mesh(bpy,points,name):
    obj = bpy.data.objects[name]
    mesh = obj.data
    print(mesh.name)
    polygon = Polygon(name)
    CoPoint = namedtuple('CoPoint', ['co'])
    for edge in mesh.edges:
        p1 = mesh.vertices[edge.vertices[0]]
        p2 = mesh.vertices[edge.vertices[1]]
        p1 = CoPoint(co=obj.matrix_world * p1.co)
        p2 = CoPoint(co=obj.matrix_world * p2.co)

        point1 = Point.findOrCreate(points,p1)
        point2 = Point.findOrCreate(points,p2)

        
        seg = Segment(None,"wall_group_segment",point1,point2)
        polygon.add_segment(seg)
    return polygon

def main(bpy):
    import _thread
    parser = Parser(bpy)
    _thread.start_new_thread(parser.parse_rooms, ("structor parse bpy rooms","") )
